Tutorial: Fix Texture Bleeding Using Shader in Godot

What is texture bleeding

Texture bleeding is a common issue in 3D graphics where colors of the texture bleed into neighboring areas.

What causes texture bleeding

Texture bleeding is caused by a combination of mipmaps & linear texture filtering.

Mipmaps

A mipmap level is a version of a texture with a specific resolution. Mipmap levels exist in sets called mipmaps. Mipmaps contain progressively smaller and lower resolution versions of a single texture. Mipmaps are intended to increase rendering speed and reduce aliasing artifacts.

For example, a mipmap might contain four versions of a texture, from the original texture (mipmap level 0), to mipmap level 1, mipmap level 2, and mipmap level 3.

Texture Filtering

Texture filtering or texture smoothing is the method used to improve the appearance of textures viewed at different angles or distances. It helps reduce artifacts when textures are stretched, shrunk or rotated.

  • Nearest-neighbor with mipmapping
    • This method still uses nearest neighbor interpolation, but adds mipmapping. This reduces the aliasing and shimmering significantly during minification but does not eliminate it entirely.
  • Linear mipmap filtering
    • OpenGL and other APIs support nearest-neighbor sampling from individual mipmaps whilst linearly interpolating the two nearest mipmaps relevant to the sample.

Lower level mipmaps appear based on the distance of the texture to the camera. When a texture is downscaled for mipmaps with linear filtering on, the texture gets blurrier each mip level. This causes sharp details at mip 0 become blurred at mip 3, causing texture bleeding.

When viewing the texture at an angle, the texture appears compressed in one direction. This causes the GPU to select a lower level mipmap and cause texture bleed.

Solution

Comparison between linear and nearest-neighbor filtering at mip 0.

linearMip0
nearestMip0

Comparison between linear and nearest filtering at mip 6. The bleed is visible on the linearly filtered image.

linearMip6
nearestMip6
Texture setup (texture is 1024x1024)
texture

Comparison between linear and nearest filtering at 35m at mip 9. The pixelated look of nearest filtering isn't noticeable at a distance.

linearMip9-35m
nearestMip9-35m

It’s clear that nearest-neighbor filtering solves our texture bleeding issue, but looks pixelated at close viewing distance. To solve that, we can use a mix of linear and nearest filtering based on distance from camera. We’ll use linear filtering when the texture is closer, nearest filtering when it’s really far away. The distance at which texture bleeding happens will depend on texture size and UV, so we have to set the distance manually.

In Godot 4, we can set the filtering option in the Texture2DParameter in the Visual Shader.

imageSampler

For Shader Code, we can set it using

textureFiltering.gdshader
    uniform sampler2D texture_albedo : source_color, filter_linear_mipmap;

We’ll use Linear Mipmap for when the texture is close to the camera, and Nearest Mipmap for when it’s far.

imageSamplers

Now we need to blend between the two samples based on distance to camera.

First we need to get the distance of VERTEX from the center of the screen.

vertexShader

Send the value from the vertex shader to the fragment shader using a varying.

varying1
varying2

In fragment shader, we use the VertexLength value to calculate and set start of transition and transition range. This is the blending factor for our two image samplers.

finalShader

Blending between two images.

blend
distBlend

And that’s it. Now we have a shader that blends between linearly filtered and nearest filtered images based on distance to camera. We set the start of transition in the start_dist parameter, and transition distance in the transition_dist parameter. Both of these values are in meters. The values are set based on the distance where the texture bleeding starts. You can see the distance to world origin by turning on Information in Godot.

info

Use Cases

This shader is useful for UVs that have little padding. Instead of adding a lot of padding to textures, this method can be used instead. It can be useful for trim sheet workflows as well.

This shader was done in Godot 4, but it can be easily implemented in Unity or UE using similar nodes.